import { createCSSTransform, createSVGTransform } from './utils/domFns';
import { canDragX, canDragY, createDraggableData, getBoundPosition } from './utils/positionFns';
import DraggableCore from './DraggableCore';
import type { ControlPosition, PositionOffsetControlPosition, DraggableCoreProps, DraggableCoreDefaultProps } from './DraggableCore';
import log from './utils/log';
import type { Bounds, DraggableEventHandler } from './utils/types';
import { batch, createComputed, onCleanup, onMount } from 'solid-js';
import { createStore } from 'solid-js/store';

export type DraggableState = {
  dragging: boolean,
  dragged: boolean,
  x: number, y: number,
  slackX: number, slackY: number,
  isElementSVG: boolean,
  prevPropsPosition: ControlPosition | undefined,
};

export type DraggableDefaultProps = {
    axis?: 'both' | 'x' | 'y' | 'none',
    bounds?: Bounds | string | false,
    classList?: any,
    class?: string,
    style?: any,
    defaultClassName?: string,
    defaultClassNameDragging?: string,
    defaultClassNameDragged?: string,
    defaultPosition?: ControlPosition,
    scale?: number,
} & DraggableCoreDefaultProps;

export type DraggableProps = {
    positionOffset?: PositionOffsetControlPosition,
    position?: ControlPosition,
} & DraggableCoreProps & DraggableDefaultProps;

//
// Define <Draggable>
//
export function Draggable(props: DraggableProps) {
    const axis = props.axis ?? 'both';
    const bounds = props.bounds ?? false;
    const defaultClassName = 'solid-draggable';
    const defaultClassNameDragging = 'solid-draggable-dragging';
    const defaultClassNameDragged = 'solid-draggable-dragged';
    const defaultPosition = props.defaultPosition ?? { x: 0, y: 0 };
    const scale = props.scale ?? 1;

    const [state, setState] = createStore<DraggableState>({
        // Whether or not we are currently dragging.
        dragging: false,

        // Whether or not we have been dragged before.
        dragged: false,

        // Current transform x and y.
        x: props.position ? props.position.x : defaultPosition.x,
        y: props.position ? props.position.y : defaultPosition.y,

        prevPropsPosition: { ...props.position },

        // Used for compensating for out-of-bounds drags
        slackX: 0, slackY: 0,

        // Can only determine if SVG after mounting
        isElementSVG: false
    });

    if (props.position && !(props.onDrag || props.onStop)) {
        // eslint-disable-next-line no-console
        console.warn('A `position` was applied to this <Draggable>, without drag handlers. This will make this ' +
            'component effectively undraggable. Please attach `onDrag` or `onStop` handlers so you can adjust the ' +
            '`position` of this element.');
    }

    onMount(() => {
        if (typeof window.SVGElement !== 'undefined' && findDOMNode() instanceof window.SVGElement) {
            setState('isElementSVG', true);
        }
    })

    onCleanup(() => {
        setState('dragging', false);
    })

    let node: any;
    const findDOMNode = (): HTMLElement => {
        return node().target;
    }

    const onDragStart: DraggableEventHandler = (e, coreData) => {
        log('Draggable: onDragStart: %j', coreData);

        // Short-circuit if user's callback killed it.
        const shouldStart = props.onStart && props.onStart(e, createDraggableData(props, state, coreData));
        // Kills start event on core as well, so move handlers are never bound.
        if (shouldStart === false) return false;

        batch(() => {
            setState('dragging', true);
            setState('dragged', true);
        })
    };

    const onDrag: DraggableEventHandler = (e, coreData) => {
        if (!state.dragging) return false;
        log('Draggable: onDrag: %j', coreData);

        const uiData = createDraggableData(props, state, coreData);

        const newState = {
            x: uiData.x,
            y: uiData.y,
            slackX: 0,
            slackY: 0
        };

        // Keep within bounds.
        if (props.bounds) {
            // Save original x and y.
            const { x, y } = newState;

            // Add slack to the values used to calculate bound position. This will ensure that if
            // we start removing slack, the element won't react to it right away until it's been
            // completely removed.
            newState.x += state.slackX;
            newState.y += state.slackY;

            // Get bound position. This will ceil/floor the x and y within the boundaries.
            const [newStateX, newStateY] = getBoundPosition(props, findDOMNode(), newState.x, newState.y);
            newState.x = newStateX;
            newState.y = newStateY;

            // Recalculate slack by noting how much was shaved by the boundPosition handler.
            newState.slackX = state.slackX + (x - newState.x);
            newState.slackY = state.slackY + (y - newState.y);

            // Update the event we fire to reflect what really happened after bounds took effect.
            uiData.x = newState.x;
            uiData.y = newState.y;
            uiData.deltaX = newState.x - state.x;
            uiData.deltaY = newState.y - state.y;
        }

        // Short-circuit if user's callback killed it.
        const shouldUpdate = props.onDrag && props.onDrag(e, uiData);
        if (shouldUpdate === false) return false;

        batch(() => {
            setState('x', newState.x);
            setState('y', newState.y);
            setState('slackX', newState.slackX);
            setState('slackY', newState.slackY);
        })
    };

    const onDragStop: DraggableEventHandler = (e, coreData) => {
        if (!state.dragging) return false;

        // Short-circuit if user's callback killed it.
        const shouldContinue = props.onStop && props.onStop(e, createDraggableData(props, state, coreData));
        if (shouldContinue === false) return false;

        log('Draggable: onDragStop: %j', coreData);

        const newState = {
            dragging: false,
            x: 0,
            y: 0,
            slackX: 0,
            slackY: 0
        };

        // If this is a controlled component, the result of this operation will be to
        // revert back to the old position. We expect a handler on `onDragStop`, at the least.
        const controlled = Boolean(props.position);
        batch(() => {
            setState('dragging', false);
            setState('slackX', newState.slackX);
            setState('slackY', newState.slackY);
            if (controlled) {
                const { x, y } = props.position;
                setState('x', x);
                setState('y', y);
            }
        })
    };

    // if controlled sync position
    createComputed(() => {
        const controlled = Boolean(props.position);
        const draggable = !controlled || state.dragging;
        const validPosition = props.position || defaultPosition;
        batch(() => {
            if (canDragX(props) && draggable) {
                setState('x', validPosition.x);
            }
            if (canDragY(props) && draggable) {
                setState('y', validPosition.y);
            }
        })
    })

    const transformOpts = () => {
        const controlled = Boolean(props.position);
        const draggable = !controlled || state.dragging;
        const validPosition = props.position || defaultPosition;
        
        return {
            // Set left if horizontal drag is enabled
            x: canDragX(props) && draggable ?
                state.x :
                validPosition.x,

            // Set top if vertical drag is enabled
            y: canDragY(props) && draggable ?
                state.y :
                validPosition.y
        };
    }

    const classList = () => ({
        ...props.classList,
        [props.class]: !!props.class,
        [defaultClassName]: defaultClassName,
        [defaultClassNameDragging]: state.dragging,
        [defaultClassNameDragged]: state.dragged
    });

    const style = () => {
        let obj = {
            ...props.style
        };
        if(!state.isElementSVG) {
            obj = Object.assign(obj, createCSSTransform(transformOpts(), props.positionOffset));
        }
        return obj;
    }

    const svgTransform = () => {
        if(state.isElementSVG) {
            return createSVGTransform(transformOpts(), props.positionOffset);
        }
        return '';
    }

    return <DraggableCore transform={svgTransform()} style={style()} classList={classList()} ref={node} {...props} onStart={onDragStart} onDrag={onDrag} onStop={onDragStop}>
        {props.children}
    </DraggableCore>
}